import * as jose from 'jose';
import CryptoJS from 'crypto-js';
import LZString from 'lz-string';
import dayjs from 'dayjs';
import type { IData, IDifference, IError, IUser, IUserOv } from '@/interfaces';
import type { JWTPayload } from 'jose/dist/types/types';
import type { Metadata } from 'next';
import type { TMetadata } from '@/types';
import avatar from '@/public/images/avatar.png';
import { StaticImageData } from 'next/image';
import type { ReadonlyURLSearchParams } from 'next/dist/client/components/navigation';

export const UNAUTHORIZED_VALUE = {
  status: 401,
  message: '访问此资源需要身份认证',
};

export const FORBIDDEN_VALUE = {
  status: 403,
  message: '访问此资源需要身份授权',
};

export const UNSUPPORTED_MEDIA_TYPE = {
  status: 415,
  message: '不支持的媒体类型',
};

export const customData = (data: any): IData<unknown> => {
  return {
    data,
    message: 'OK',
    status: 200,
  };
};

export const customException = (status = 500, message = '未知错误'): IError => {
  return {
    status,
    message,
  };
};

export const isEmpty = (data: {} | []) => {
  return Array.isArray(data)
    ? data.length === 0
    : Object.keys(data).length === 0;
};

export const isPhone = (phone: string) => {
  return /^1[3-9]\d{9}$/.test(phone);
};

export const aesEncryptObj = (obj: object, key: string) => {
  return CryptoJS.AES.encrypt(JSON.stringify(obj), key).toString();
};

export const aesDecryptObj = (str: string, key: string) => {
  const bytes = CryptoJS.AES.decrypt(str, key);
  return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
};

export const aesEncryptStr = (str: string, key: string) => {
  return CryptoJS.AES.encrypt(str, key).toString();
};

export const aesDecryptStr = (str: string, key: string) => {
  const bytes = CryptoJS.AES.decrypt(str, key);
  return bytes.toString(CryptoJS.enc.Utf8);
};

export const compressToUTF16 = (str: string) => {
  return LZString.compressToUTF16(str);
};

export const decompressFromUTF16 = (str: string) => {
  return LZString.decompressFromUTF16(str);
};

export const decodeJwtExpToSeconds = (jwt: string): number | undefined => {
  return jwtExpToSeconds(jose.decodeJwt(jwt).exp);
};

export const decodeJwt = (jwt: string): JWTPayload => {
  return jose.decodeJwt(jwt);
};

export const jwtExpToSeconds = (
  exp: number | undefined,
): number | undefined => {
  if (!exp) {
    return;
  }
  return dayjs.unix(exp).diff(dayjs(), 'seconds');
};

export const range = (start: number, end: number) => {
  const length = end - start + 1;
  return Array.from({ length }, (value, index) => index + start);
};

export const isHttpOrHttps = (value: string) => {
  return value.startsWith('http') || value.startsWith('https');
};

export const getDiffData = (data: IDifference[] = []) => {
  const obj: Record<string, any> = {};
  for (const { path, value } of data) {
    const [key1, key2] = path;
    let target = obj[key1];
    if (key2 === undefined) {
      target = obj[key1] = value;
    } else {
      target = target || (typeof key2 === 'number' ? [] : {});
      target[key2] = value;
    }
  }
  return obj;
};

export const getFilterData = (
  data: any,
  hitList: string[] = [],
  hitFn: (key: any, value: any) => boolean = () => true,
): { [k: string]: any } => {
  if (typeof data !== 'object') {
    return data;
  }

  const obj: { [k: string]: any } = {};
  for (const dataKey in data) {
    if (!hitList.includes(dataKey)) {
      const value = data[dataKey];
      if (
        typeof value === 'boolean' ||
        typeof value === 'number' ||
        typeof value === 'string'
      ) {
        obj[dataKey] = value;
      } else if (
        typeof value === 'object' &&
        ((Array.isArray(value) && value.length !== 0) ||
          Object.keys(value).length !== 0)
      ) {
        obj[dataKey] = Array.isArray(value)
          ? value
          : getFilterData(value, hitList, hitFn);
      }
    } else if (hitFn(dataKey, data[dataKey])) {
      obj[dataKey] = data[dataKey];
    }
  }

  const newObj: { [k: string]: any } = {};
  for (const objKey in obj) {
    const value = obj[objKey];
    if (
      (typeof value === 'object' && Array.isArray(value) && value.length > 0) ||
      Object.keys(value).length > 0
    ) {
      newObj[objKey] = value;
    } else if (
      typeof value === 'boolean' ||
      typeof value === 'number' ||
      typeof value === 'string'
    ) {
      newObj[objKey] = value;
    }
  }

  return newObj;
};

export const isNull = (value: string | undefined | null) => {
  return value === null || value === undefined || value === '';
};

export const nonNull = (value: string | undefined | null) => {
  return !isNull(value);
};

export const shuffle = (arr: any[]): any[] => {
  for (let i = 1; i < arr.length; i++) {
    const random = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[random]] = [arr[random], arr[i]];
  }
  return arr;
};

export const isNum = (val: any) => {
  return !isNaN(parseInt(val));
};

export const scrollToBottom = () => {
  scrollTo(0, document.body.scrollHeight);
};

export const toRelativeTime = (time: string) => {
  return dayjs().utc().local().to(dayjs(time).utc());
};

export const formatTime = (time: string) => {
  return dayjs(time).format('YYYY/MM/DD');
};

export const formatDate = (date: string) => {
  return dayjs(date).format('YYYY-MM-DD HH:mm:ss');
};

export const getMetadata = (
  options: Metadata & { index?: boolean } = {
    title: 'App',
    index: true,
  },
): Metadata => {
  const _metadata: Metadata = {
    ...options,
    title: `${options.title} - ${process.env.APP_URL_HOST}`,
    applicationName: process.env.APP_NAME,
    generator: process.env.APP_NAME,
  };

  if (!_metadata.robots) {
    if (options.index === undefined || options.index) {
      _metadata.robots = {
        index: true,
        follow: true,
      };
    } else {
      _metadata.robots = {
        index: false,
        follow: false,
      };
    }
  }

  return _metadata;
};

export const getUserAvatar = (
  user: IUserOv | IUser | null | undefined,
  metadata: TMetadata,
): {
  smallAvatarUrl: string | StaticImageData;
  mediumAvatarUrl: string | StaticImageData;
  largeAvatarUrl: string | StaticImageData;
} => {
  const env = metadata.env;
  const urls: {
    smallAvatarUrl: string | StaticImageData;
    mediumAvatarUrl: string | StaticImageData;
    largeAvatarUrl: string | StaticImageData;
  } = {
    smallAvatarUrl: avatar,
    mediumAvatarUrl: avatar,
    largeAvatarUrl: avatar,
  };

  if (!user) {
    return urls;
  }

  const smallAvatarUrl = user.details!.smallAvatarUrl;
  const mediumAvatarUrl = user.details!.mediumAvatarUrl;
  const largeAvatarUrl = user.details!.largeAvatarUrl;

  if (smallAvatarUrl) {
    urls.smallAvatarUrl = isHttpOrHttps(smallAvatarUrl)
      ? smallAvatarUrl
      : env.APP_OSS_SERVER + smallAvatarUrl;
  }

  if (mediumAvatarUrl) {
    urls.mediumAvatarUrl = isHttpOrHttps(mediumAvatarUrl)
      ? mediumAvatarUrl
      : env.APP_OSS_SERVER + mediumAvatarUrl;
  }

  if (largeAvatarUrl) {
    urls.largeAvatarUrl = isHttpOrHttps(largeAvatarUrl)
      ? largeAvatarUrl
      : env.APP_OSS_SERVER + largeAvatarUrl;
  }
  return urls;
};

export const getCover = (cover: string, metadata: TMetadata) => {
  return isHttpOrHttps(cover) ? cover : metadata.env.APP_OSS_SERVER + cover;
};

export const cleanFormData = (form: Record<string, any> = {}): any => {
  const obj: Record<string, any> = {};
  for (const [formKey, formElement] of Object.entries(form)) {
    switch (typeof formElement) {
      case 'string':
        obj[formKey] = '';
        break;
      case 'boolean':
        obj[formKey] = false;
        break;
      case 'number':
        obj[formKey] = 0;
        break;
      case 'object':
        if (Array.isArray(formElement)) {
          obj[formKey] = [];
        } else if (formElement && Object.keys(formElement).length > 0) {
          obj[formKey] = cleanFormData(formElement);
        } else {
          obj[formKey] = formElement;
        }
        break;
      default:
        obj[formKey] = formElement;
    }
  }
  return obj;
};

export const getRandomIntInclusive = (min: number, max: number) => {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min;
};

export const getRandomElement = (arr: unknown[]): unknown | undefined => {
  return arr[Math.floor(Math.random() * arr.length)];
};

export const decodeHtml = (html: string) => {
  const obj: Record<string, string> = {
    '&amp;': '&',
    '&lt;': '<',
    '&gt;': '>',
    '&quot;': '"',
    '&#x27;': "'",
    '&#x2F;': '/',
  };
  return html.replace(
    /&amp;|&lt;|&gt;|&quot;|&#x27;|&#x2F;/g,
    (key) => obj[key],
  );
};

export const delayedWait = async (
  timeout?: number | Function,
  callback?: () => void,
) => {
  await new Promise((resolve) => {
    setTimeout(
      () => {
        if (typeof callback === 'function') {
          callback();
        } else if (typeof timeout === 'function') {
          timeout();
        }
        resolve(true);
      },
      typeof timeout === 'number' ? timeout : 5000,
    );
  });
};

export const mergeObject = (
  target: Record<string, any> = {},
  source: Record<string, any> = {},
) => {
  const obj: Record<string, any> = {};
  for (let sourceKey in source) {
    if (sourceKey in target) {
      obj[sourceKey] = target[sourceKey];
    }
  }
  return obj;
};

export const toCamelCase = (str: string) => {
  const split = str.split('-');
  for (let i = 1; i < split.length; i++) {
    split[i] = split[i].charAt(0).toUpperCase() + split[i].substring(1);
  }
  return split.join('');
};

export const getRandomRgbColor = () => {
  const r = Math.floor(Math.random() * 256);
  const g = Math.floor(Math.random() * 256);
  const b = Math.floor(Math.random() * 256);
  return `rgb(${r},${g},${b})`;
};

export const generateRandomLinearGradientColor = () => {
  const angle = Math.floor(Math.random() * 361);
  return `linear-gradient(${angle}deg, ${getRandomRgbColor()}, ${getRandomRgbColor()})`;
};

export const formatCount = (count: number | null | undefined): string => {
  if (typeof count !== 'number') {
    return '0';
  }

  if (count < 1000) {
    return count + '';
  }

  let suffix = '';
  let divisor = 1;

  if (count >= 1000000000) {
    suffix = 'B';
    divisor = 1000000000;
  } else if (count >= 1000000) {
    suffix = 'M';
    divisor = 1000000;
  } else if (count >= 1000) {
    suffix = 'K';
    divisor = 1000;
  }

  return (count / divisor).toFixed(1) + suffix;
};

export const getSectionQueryParams = (
  searchParams: ReadonlyURLSearchParams,
): any | undefined => {
  if (searchParams.has('sectionId')) {
    return {
      id: parseInt(searchParams.get('sectionId')!),
    } as any;
  } else if (searchParams.has('sid')) {
    return {
      id: parseInt(searchParams.get('sid')!),
    } as any;
  }
};

export const getTagQueryParams = (
  searchParams: ReadonlyURLSearchParams,
): any | undefined => {
  if (searchParams.has('tagId')) {
    return {
      id: parseInt(searchParams.get('tagId')!),
    } as any;
  } else if (searchParams.has('tid')) {
    return {
      id: parseInt(searchParams.get('tid')!),
    } as any;
  }
};

export const getPostQueryParams = (
  searchParams: ReadonlyURLSearchParams,
): any | undefined => {
  if (searchParams.has('postId')) {
    return {
      id: parseInt(searchParams.get('postId')!),
    } as any;
  } else if (searchParams.has('pid')) {
    return {
      id: parseInt(searchParams.get('pid')!),
    } as any;
  }
};

export const getUserQueryParams = (
  searchParams: ReadonlyURLSearchParams,
): any | undefined => {
  if (searchParams.has('userId')) {
    return {
      id: parseInt(searchParams.get('userId')!),
    } as any;
  } else if (searchParams.has('uid')) {
    return {
      id: parseInt(searchParams.get('uid')!),
    } as any;
  }
};

export const parseMessage = (message: any): string => {
  if (message === undefined || message === null) {
    return '';
  }

  if (typeof message === 'object' && 'message' in message) {
    return parseMessage(message.message);
  }

  if (message instanceof Error) {
    return parseMessage(message.message);
  }

  return String(message);
};

export const isValidLink = (link: string) => {
  return /^(http|https):\/\//.test(link);
};

export const composeMiddlewares = (...funcs: Function[]) => {
  return funcs.length === 0
    ? (arg: any) => arg
    : funcs.length === 1
    ? funcs[0]
    : funcs.reduce(
        (a, b) =>
          (...args: any[]) =>
            b(a(...args)),
      );
};
